﻿//////////////////////////////////////////////////////////////////
//
// Main.cs
//
// Copyright (c) 2012 Dan Pike.
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation files
// (the "Software"), to deal in the Software without restriction,
// including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software,
// and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
using Antlr.Runtime;
using Entities;
using Entities.Cities;
using Logger4Net;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace Sally
{
   public partial class Main : Form
   {
      //////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// 
      /// </summary>
      public Main()
      {
         try
         {
            logger_.debugFormat("creating GUI");

            InitializeComponent();

            normalFont_ = lvCity.Font;
            italicFont_ = new Font(normalFont_, FontStyle.Italic);

            panelMaterial.Dock = DockStyle.Fill;
            flowSummary.Dock = DockStyle.Top;
            lblCity.Height = lblConvoy.Height = flowSummary.Height;

            lblCity.Dock = DockStyle.Top;
            lvCity.Dock = DockStyle.Fill;
            lvCity.HideSelection = false;
            lvCity.Columns.Add("name").Width = 100;
            lvCity.Columns.Add("factories").Width = 100;
            layoutButtons.Dock = DockStyle.Bottom;

            lblConvoy.Dock = DockStyle.Top;
            lvConvoy.Dock = DockStyle.Fill;
            lvConvoy.HideSelection = false;
            lvConvoy.Columns.Add("name").Width = 100;
            lvConvoy.Columns.Add("capacity").Width = 80;
            lvConvoy.Columns.Add("required").Width = 80;

            lvQuantity.Dock = DockStyle.Fill;
            lvQuantity.HideSelection = false;
            lvQuantity.Columns.Add("material");
            lvQuantity.Columns.Add("load").TextAlign = HorizontalAlignment.Center;
            lvQuantity.Columns.Add("buy").TextAlign = HorizontalAlignment.Center;
            lvQuantity.Columns.Add("sell").TextAlign = HorizontalAlignment.Center;
            lvQuantity.Columns.Add("factory").TextAlign = HorizontalAlignment.Center;
            lvQuantity.Columns.Add("required").TextAlign = HorizontalAlignment.Center;
            lvQuantity.Columns.Add("price").TextAlign = HorizontalAlignment.Center;

            totalItem_ = lvQuantity.Items.Add("total:");
            for (int index = 1; index < lvQuantity.Columns.Count; ++index)
            {
               totalItem_.SubItems.Add((5 >= index) ? "0" : string.Empty);
            }
            lvQuantity.Items.Add("");

            logger_.debugFormat("creating World");
            world_ = new World();

            world_.almanac.materials.ForEach(material => material.growthRatio = Program.settings.growthRatios[material.name].ratio);
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// 
      /// </summary>
      /// <param name="sender"></param>
      /// <param name="e"></param>
      private void Main_Load(object sender, EventArgs e)
      {
         try
         {
            // Place a watch on the world info file
            worldInfoFilename_ = Path.Combine(
               Environment.ExpandEnvironmentVariables("%APPDATA%"),
               "Kalypso Media",
               "Patrician 4 Addon",
               "worldinfo.txt");
            worldInfoDate_ = DateTime.MinValue;

            // Load the size and position from configuration
            Rectangle position = Program.settings.position;
            if (0 < position.Width)
            {
               StartPosition = FormStartPosition.Manual;
               if (-1 == position.Height)
               {
                  this.WindowState = FormWindowState.Minimized;
               }
               else if (0 > position.Height)
               {
                  this.WindowState = FormWindowState.Maximized;
               }
               else
               {
                  this.WindowState = FormWindowState.Normal;

                  // Make sure that we can see it!
                  Rectangle display = Screen.AllScreens[0].WorkingArea;
                  Screen.AllScreens.OfType<Screen>().Skip(1).ToList().ForEach(screen =>
                     {
                        Rectangle workingArea = screen.WorkingArea;
                        display.X = Math.Min(display.X, workingArea.X);
                        display.Y = Math.Min(display.Y, workingArea.Y);
                        display.Width = Math.Min(display.Width, workingArea.Width);
                        display.Height = Math.Min(display.Height, workingArea.Height);
                     });

                  this.Height = Math.Max(40, position.Height);
                  this.Width = Math.Max(40, position.Width);
                  this.Top = Math.Min(display.Y + display.Height + 40 - this.Height, position.Top);
                  this.Left = Math.Max(display.X + 40 - this.Width, position.Left);
               }
            }
            Program.settings.views.OfType<Config.View>().ToList().ForEach(configView =>
            {
               Control[] controls = this.Controls.Find(configView.name, true);
               ListView view = (1 == controls.Length) ? controls[0] as ListView : null;
               if (null == view)
               {
                  logger_.errorFormat("could not find view called '{0}' to set column width",
                     configView.name);
                  Program.settings.views.Remove(configView);
               }
               else
               {
                  configView.columns.OfType<Config.Column>().ToList().ForEach(configColumn =>
                  {
                     ColumnHeader columnHeader = view.Columns.OfType<ColumnHeader>()
                        .FirstOrDefault(qColumnHeader => qColumnHeader.Text == configColumn.title);
                     if (null == columnHeader)
                     {
                        logger_.errorFormat("could not find column called '{0}' on view '{1}' to set column width",
                           configColumn.title, configView.name);
                        configView.columns.Remove(configColumn);
                     }
                     else
                     {
                        columnHeader.Width = configColumn.width;
                     }
                  });
               }
            });
            splitCityMaterial.SplitPosition = Program.settings.splitCityMaterial;
            splitConvoyCity.SplitPosition = Program.settings.splitConvoyCity;

            // And start the worldinfo watcher
            guiTimer.Interval = 1000;
            guiTimer.Enabled = true;
            handleTick(null, null);
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// 
      /// </summary>
      /// <param name="sender"></param>
      /// <param name="e"></param>
      private void Main_Shown(object sender, EventArgs e)
      {
         try
         {
            // Fill in the materials
            world_.almanac.materials.ForEach(material =>
               {
                  ListViewItem item = lvQuantity.Items.Add(material.name);
                  item.BackColor = (0 == items_.Count % 3) ? SystemColors.MenuBar : SystemColors.Window;
                  item.Tag = material;
                  for (int index = 1; index < lvQuantity.Columns.Count - 1; ++index)
                  {
                     item.SubItems.Add(string.Empty);
                  }
                  item.SubItems.Add(string.Concat(
                     material.buyPrice,
                     " / ",
                     material.sellPrice,
                     string.Empty
                     ));
                  items_.Add(material, item);
               });

            // Now stretch the columns to fit the available space
            //handleResize(lvConvoy, null);
            //handleResize(lvCity, null);
            //handleResize(lvQuantity, null);
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// 
      /// </summary>
      /// <param name="sender"></param>
      /// <param name="e"></param>
      private void frozenHarboursToolStripMenuItem_Click(object sender, EventArgs e)
      {
         try
         {
            frozenHarbours_ = !frozenHarbours_;
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// 
      /// </summary>
      /// <param name="sender"></param>
      /// <param name="e"></param>
      private void configToolStripMenuItem_DropDownOpening(object sender, EventArgs e)
      {
         try
         {
            frozenHarboursToolStripMenuItem.Checked = frozenHarbours_;
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// 
      /// </summary>
      /// <param name="sender"></param>
      /// <param name="e"></param>
      private void handleTick(object sender, EventArgs e)
      {
         try
         {
            UseWaitCursor = true;

            // Collect all of the information from worldinfo.txt
            WorldInfoData infoFile = parseWorldInfo();
            if (null != infoFile)
            {
               // Copy the information into the World
               updateCitiesFromWorldInfo(infoFile);

               // Recalculate the city demands
               world_.cities.ForEach(city => city.recalculate());

               // Update the convoys and required payloads
               updateConvoysFromWorldInfo(infoFile);

               // Update the display
               handleSelectedIndexChanged(lvConvoy, null);
               handleSelectedIndexChanged(lvCity, null);
               handleSelectedIndexChanged(lvQuantity, null);
            }
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
         finally
         {
            UseWaitCursor = false;
         }
      }

      //////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// 
      /// </summary>
      /// <param name="sender"></param>
      /// <param name="e"></param>
      private void handleSelectedIndexChanged(object sender, EventArgs e)
      {
         if (0 < recursionLockSelectedIndexChanged_)
         {
            return;
         }

         try
         {
            ++recursionLockSelectedIndexChanged_;

            lvQuantity.BeginUpdate();
            lvConvoy.BeginUpdate();
            lvCity.BeginUpdate();

            // Need to clear all the highlights because we build them up as we go
            // along and it may be that a city is in one convoy but not the next
            lvQuantity.Items.Cast<ListViewItem>().ToList().ForEach(item => item.Font = normalFont_);
            lvCity.Items.Cast<ListViewItem>().ToList().ForEach(item =>
            {
               item.Font = normalFont_;

               // The Qty field for cities depends on the selected materials,
               // so we clear the "total" stored in the tag and build it up as
               // we go along
               ListViewItem.ListViewSubItem subItem = item.SubItems[1];
               subItem.Tag = 0;
               subItem.Text = string.Empty;
            });

            if ((sender == lvConvoy) || (sender == lvCity))
            {
               lvQuantity.SelectedItems.Clear();

               QuantityResult result = new QuantityResult(world_);

               // Total up all of the quantities for the selected cities + convoys
               List<ListViewItem> selectedItems = lvConvoy.SelectedItems.OfType<ListViewItem>().ToList();
               selectedItems.AddRange(lvCity.SelectedItems.OfType<ListViewItem>());
               List<SallyConvoy> selectedConvoys = recalculateQuantity(selectedItems, result);

               if (null != selectedConvoys)
               {
                  // The maximum percentage is 100% only if the convoy has enough capacity
                  int totalRequired = world_.almanac.materials.Sum(material => result.totalLoadFromHub[material]);

                  // Spread the load across the selected convoys and scale to the available capacity
                  spreadAcrossConvoys(totalRequired, selectedConvoys, result);
               }

               // Display the results
               displayQuantities(result);
               lblPopulation.Text = String.Concat("Population: ", result.population.ToString("#,#"));
               lblCapacity.Text = String.Concat("Capacity: ", result.capacity.ToString("#,#"));
            }
            else if (sender == lvQuantity)
            {
               lvConvoy.SelectedItems.Clear();
               lvCity.SelectedItems.Clear();

               // Highlight all of the cities that are capable of producing this
               List<Factory> factories = lvQuantity.SelectedItems.Cast<ListViewItem>()
                  .Select<ListViewItem, Factory>(item => (item.Tag as Material).producer).ToList();
               factories.ForEach(producer => lvCity.Items.Cast<ListViewItem>()
                  .Select<ListViewItem, SallyCity>(qItem => qItem.Tag as SallyCity)
                     .Where(sallyCity => sallyCity.city.factories.ContainsKey(producer)).ToList()
                        .ForEach(sallyCity =>
                        {
                           ListViewItem item = sallyCity.item;
                           item.Font = italicFont_;

                           ListViewItem.ListViewSubItem subItem = item.SubItems[1];
                           int factoryCount = sallyCity.city.factories[producer] + (int)subItem.Tag;
                           subItem.Tag = factoryCount;
                           subItem.Text = factoryCount.ToString("#,#");
                        }));
            }
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
         finally
         {
            lvQuantity.EndUpdate();
            lvCity.EndUpdate();
            lvConvoy.EndUpdate();
            --recursionLockSelectedIndexChanged_;
         }
      }

      //////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// 
      /// </summary>
      /// <param name="selectedConvoys"></param>
      /// <param name="result"></param>
      private void spreadAcrossConvoys(int totalRequiredCapacity,
         List<SallyConvoy> selectedConvoys, QuantityResult result)
      {
         try
         {
            double totalAvailableCapacity = selectedConvoys.Sum(convoy => convoy.capacity);
            double maxPercent = Math.Min(100.0, 100.0 * totalAvailableCapacity / totalRequiredCapacity);
            selectedConvoys.ForEach(convoy =>
               {
                  int percent = (int)Math.Ceiling(maxPercent * (double)convoy.capacity / totalAvailableCapacity);
                  result.setConvoyPercent(convoy, percent);
               });
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }      

      //////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// 
      /// </summary>
      /// <param name="sender"></param>
      /// <param name="e"></param>
      private void handleResize(object sender, EventArgs e)
      {
         //ListView view = sender as ListView;
         //try
         //{
         //   view = sender as ListView;
         //   if (null == view)
         //   {
         //      return;
         //   }
         //   view.BeginUpdate();

         //   int count = (null == view) ? 0 : view.Columns.Count;
         //   if (0 == count)
         //   {
         //      // Not ready
         //      return;
         //   }

         //   if ((view == lvConvoy) || (view == lvCity))
         //   {
         //      // Size the left-most column as large as possible
         //      if (0 < count)
         //      {
         //         int remaining = view.ClientSize.Width;
         //         if (1 < count)
         //         {
         //            remaining -= view.Columns.Cast<ColumnHeader>()
         //               .Skip(1).Sum(column => column.Width);
         //         }
         //         view.Columns[0].Width = Math.Max(40, remaining);
         //      }
         //   }
         //   else if (view == lvQuantity)
         //   {
         //      // All columns the same width
         //      int equalWidth = view.ClientSize.Width / count;
         //      view.Columns.Cast<ColumnHeader>().ToList()
         //         .ForEach(column => column.Width = equalWidth);
         //   }
         //}
         //catch (System.Exception ex)
         //{
         //   logger_.exception(ex);
         //}
         //finally
         //{
         //   if (null != view)
         //   {
         //      view.EndUpdate();
         //   }
         //}
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private void btnAllCities_Click(object sender, EventArgs e)
      {
         try
         {
            lvCity.Items.OfType<ListViewItem>().ToList().ForEach(item => item.Selected = true);
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private WorldInfoData parseWorldInfo()
      {
         try
         {
            WorldInfoData result = null;
            DateTime fileTime = File.GetLastWriteTime(worldInfoFilename_);
            if (fileTime > worldInfoDate_)
            {
               result = new WorldInfoData(world_);

               // Parse the file
               string allFile = File.ReadAllText(worldInfoFilename_, Encoding.Unicode);
               MemoryStream stream = new MemoryStream(new System.Text.UnicodeEncoding().GetBytes(allFile));
               ANTLRInputStream input = new ANTLRInputStream(stream, Encoding.Unicode);
               WorldInfoFileLexer lexer = new WorldInfoFileLexer(input);
               CommonTokenStream tokens = new CommonTokenStream(lexer);
               WorldInfoFileParser parser = new WorldInfoFileParser(tokens);
               parser.world = world_;
               parser.info = result;
               logger_.debugFormat("parsing world info file '{0}'", worldInfoFilename_);
               parser.parseWorld();

               worldInfoDate_ = fileTime;

               statusText.BackColor = SystemColors.Control;
               statusText.Text = string.Concat("parsed worldinfo file @", DateTime.Now.ToString("hh:mm:ss"));
            }
            return result;
         }
         catch (MismatchedTokenException ex)
         {
            logger_.exception("\r\n{0}({1}) : mismatched token at offset {2}\r\n", ex,
               worldInfoFilename_, ex.Line, ex.CharPositionInLine);
         }
         catch (RecognitionException ex)
         {
            logger_.exception("\r\n{0}({1}) : recognition error at offset {2}\r\n", ex,
               worldInfoFilename_, ex.Line, ex.CharPositionInLine);
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
         statusText.BackColor = Color.Red;
         statusText.Text = string.Concat("failed to parse worldinfo file @", DateTime.Now.ToString("hh:mm:ss"));
         return null;
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private void updateCitiesFromWorldInfo(WorldInfoData infoFile)
      {
         try
         {
            cities_.ForEach(sallyCity =>
               {
                  WorldInfoCity infoCity;
                  if (!infoFile.infoCities.TryGetValue(sallyCity.city, out infoCity))
                  {
                     removeCity(sallyCity);
                  }
                  else
                  {
                     // Check to see if this city
                     updateCity(sallyCity, infoCity);
                  }

                  // Finished checking this city
                  infoFile.infoCities.Remove(sallyCity.city);
               });

            // Add any new cities
            infoFile.infoCities.All(kvp =>
            {
               addCity(kvp.Key, kvp.Value);
               return true;
            });
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private void addCity(City city, WorldInfoCity infoCity)
      {
         try
         {
            SallyCity sallyCity = new SallyCity();
            if (!sallyCity.initialize(world_, this, city))
            {
               logger_.errorFormat("failed to initialize city '{0}'", city.name);
               return;
            }
            cities_.Add(sallyCity);
            updateCity(sallyCity, infoCity);
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private void updateCity(SallyCity sallyCity, WorldInfoCity infoCity)
      {
         try
         {
            // Update the cities
            City city = infoCity.city;
            city.population = infoCity.population;

            city.factories.Keys.Cast<Factory>().ToList()
               .ForEach(factory => city.factories[factory] = 0);
            infoCity.factories.Cast<KeyValuePair<Factory, int>>().ToList()
               .ForEach(kvp => city.factories[kvp.Key] = kvp.Value);
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private void removeCity(SallyCity sallyCity)
      {
         try
         {
            sallyCity.shutdown();
            cities_.Remove(sallyCity);
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private void updateConvoysFromWorldInfo(WorldInfoData infoFile)
      {
         try
         {
            convoys_.ForEach(convoy =>
               {
                  List<City> cityList;
                  if (!infoFile.infoConvoyPorts.TryGetValue(convoy.name, out cityList))
                  {
                     // This convoy is no longer active
                     removeConvoy(convoy);
                  }
                  else
                  {
                     // Check to see if this convoy needs to be updated
                     updateConvoy(convoy, infoFile.infoConvoyCapacities[convoy.name], cityList);
                  }

                  // Finished checking this convoy
                  infoFile.infoConvoyPorts.Remove(convoy.name);
               });

            // Add any new convoys
            infoFile.infoConvoyPorts.All(kvp =>
               {
                  addConvoy(kvp.Key, infoFile.infoConvoyCapacities[kvp.Key], kvp.Value);
                  return true;
               });
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private void addConvoy(string name, int capacity, List<City> cities)
      {
         try
         {
            SallyConvoy convoy = new SallyConvoy();
            if (!convoy.initialize(world_, this, name))
            {
               logger_.errorFormat("failed to initialize convoy '{0}'", name);
               return;
            }
            convoys_.Add(convoy);
            updateConvoy(convoy, capacity, cities);
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private void updateConvoy(SallyConvoy convoy, int capacity, List<City> cities)
      {
         try
         {
            convoy.cities = cities;
            convoy.capacity = capacity;
            
            // This will update the payload
            recalculateQuantity(convoy, new QuantityResult(world_));
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private void removeConvoy(SallyConvoy convoy)
      {
         try
         {
            convoy.shutdown();
            convoys_.Remove(convoy);
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private void exitToolStripMenuItem_Click(object sender, EventArgs e)
      {
         try
         {
            Close();
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      public ListView viewConvoy
      {
         get { return lvConvoy; }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      public ListView viewCity
      {
         get { return lvCity; }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private List<SallyConvoy> recalculateQuantity(List<ListViewItem> selected,
         QuantityResult quantities)
      {
         List<SallyConvoy> result = null;
         try
         {
            // Now safe to scan for quantities
            selected.ForEach(item =>
               {
                  // If one of the selections is a convoy, then we are building the hub-loading
                  // counts, so we should return a list of the convoys
                  SallyConvoy sallyConvoy = item.Tag as SallyConvoy;
                  if (null != sallyConvoy)
                  {
                     quantities.selectedConvoy = recalculateQuantity(sallyConvoy, quantities) || quantities.selectedConvoy;
                     if (null == result)
                     {
                        result = new List<SallyConvoy>();
                     }
                     result.Add(sallyConvoy);
                     quantities.capacity += sallyConvoy.capacity;
                  }

                  // If we have selected cities, then we are building the totals for keeping in the
                  // counting house
                  SallyCity sallyCity = item.Tag as SallyCity;
                  if (null != sallyCity)
                  {
                     quantities.selectedCity = recalculateQuantity(sallyCity, quantities) || quantities.selectedCity;
                     quantities.population += sallyCity.city.population;
                  }
               });
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
            result = null;
         }
         return result;
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private bool recalculateQuantity(SallyConvoy sallyConvoy,
         QuantityResult result)
      {
         try
         {
            if (null == sallyConvoy)
            {
               return false;
            }

            // highlight the cities in the convoy
            lvCity.Items.OfType<ListViewItem>().ToList().ForEach(item =>
               {
                  if (sallyConvoy.getVisitsCity((item.Tag as SallyCity).city))
                  {
                     item.Font = italicFont_;
                  }
               });

            // Total up the amount that the hub will need to load
            result.totalLoadFromHub.Keys.ToList().ForEach(material =>
               result.totalLoadFromHub[material] += sallyConvoy.cities.Sum(city =>
               {
                  if (!sallyConvoy.getIsHub(city))
                  {
                     int duration = (int)Math.Ceiling(sallyConvoy.getResupplyTime(city, frozenHarbours_));
                     int popDemand = (int)Math.Ceiling(city.getPopulationDemand(material));
                     int prodDemand = (int)Math.Ceiling(city.getProductionDemand(material));
                     return duration * (popDemand + prodDemand);
                  }
                  return 0;
               })
            );

            // Calculate the payload that we could carry, if we had enough ships
            sallyConvoy.payload = result.totalLoadFromHub.Values.Sum();
            return true;
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
         return false;
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private bool recalculateQuantity(SallyCity sallyCity, QuantityResult result)
      {
         try
         {
            if (null == sallyCity)
            {
               return false;
            }
            City city = sallyCity.city;

            // highlight the convoys that visit the city
            lvConvoy.Items.OfType<ListViewItem>().ToList().ForEach(qItem =>
               {
                  if ((qItem.Tag as SallyConvoy).getVisitsCity(city))
                  {
                     qItem.Font = italicFont_;
                  }
               });

            // We total the amount that we should store in the counting house
            // Assume that all convoys are capable of supplying all the city's needs, we find
            // the minimum resupply time across all convoys that visit the city
            List<SallyConvoy> convoys = convoys_.Where(qSallyConvoy =>
               !qSallyConvoy.getIsHub(city) && qSallyConvoy.getVisitsCity(city)).ToList();

            int duration = 0;
            if (0 == convoys.Count)
            {
               // It's the hub, so we reserve seven day's supply (no particular reason for seven)
               duration = 7;
            }
            else
            {
               duration = convoys.Min(qSallyConvoy =>
                  (int)Math.Ceiling(qSallyConvoy.getResupplyTime(city, frozenHarbours_)));
            }
            if (0 < duration)
            {
               result.protectFromTrade.Keys.ToList().ForEach(material =>
               {
                  int popDemand = (int)Math.Ceiling(city.getPopulationDemand(material));
                  int prodDemand = (int)Math.Ceiling(city.getProductionDemand(material));
                  result.protectFromTrade[material] += duration * (popDemand + prodDemand);
                  result.protectFromMarket[material] += duration * prodDemand;
                  if ((null != material.producer) && city.factories.ContainsKey(material.producer))
                  {
                     result.factoryCount[material] += city.factories[material.producer];
                  }
               });
            }
            return true;
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
         return false;
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private void displayQuantities(QuantityResult result)
      {
         try
         {
            // Fill the display with the required quantities
            string display;
            int length = lvQuantity.Columns.Count - 2;
            int[] qtys = new int[length];
            int[] totals = new int[length];
            lvQuantity.Items.Cast<ListViewItem>().ToList().ForEach(item =>
            {
               Material material = item.Tag as Material;
               if (null != material)
               {
                  qtys[0] = result.totalLoadFromHub[material];
                  qtys[1] = result.protectFromTrade[material];
                  qtys[2] = result.protectFromMarket[material];
                  qtys[3] = result.factoryCount[material];
                  qtys[4] = (0 < material.count) ? (int)Math.Ceiling((result.population * material.perPopulation) / material.count) : 0;

                  for (int index = 0; index < qtys.Length; ++index)
                  {
                     totals[index] += qtys[index];

                     if (0 == index)
                     {
                        // Display the load-hub quantities split across the convoy
                        StringBuilder sb = new StringBuilder();

                        result.convoys.ForEach(convoy =>
                           {
                              if (0 < sb.Length)
                              {
                                 sb.Append("+");
                              }
                              sb.Append(result.getLoadFromHub(material, convoy).ToString("#,#"));
                           });
                        if (1 < result.convoyPercentCount)
                        {
                           sb.Append('=');
                           sb.Append(qtys[index].ToString("#,#"));
                        }
                        display = sb.ToString();
                     }
                     else
                     {
                        // Display the raw value
                        display = qtys[index].ToString("#,#");
                     }

                     item.SubItems[index + 1].Text = display;
                  }
               }
            });

            for (int index = 0; index < totals.Length; ++index)
            {
               totalItem_.SubItems[index + 1].Text = totals[index].ToString("#,#");
            }
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private void Main_FormClosing(object sender, FormClosingEventArgs e)
      {
         try
         {
            // Save the position to the configuration
            switch (this.WindowState)
            {
               case FormWindowState.Minimized:
                  Program.settings.position = new Rectangle(0, 0, 0, -1);
                  break;

               case FormWindowState.Maximized:
                  Program.settings.position = new Rectangle(0, 0, 0, 2);
                  break;

               case FormWindowState.Normal:
                  Program.settings.position = new Rectangle(this.Location, this.Size);
                  break;
            }
            new ListView[] { lvCity, lvConvoy, lvQuantity }.ToList().ForEach(view =>
               {
                  Config.View configView = Program.settings.views[view.Name];
                  view.Columns.OfType<ColumnHeader>().ToList().ForEach(columnHeader =>
                     {
                        Config.Column configColumn = configView.columns[columnHeader.Text];
                        configColumn.width = columnHeader.Width;
                     });
               });

            Program.settings.splitCityMaterial = splitCityMaterial.SplitPosition;
            Program.settings.splitConvoyCity = splitConvoyCity.SplitPosition;
         }
         catch (System.Exception ex)
         {
            logger_.exception(ex);
         }
      }

      //////////////////////////////////////////////////////////////////////////
      //
      private static Logger logger_ = new Logger(typeof(Main));
      private string worldInfoFilename_;
      private DateTime worldInfoDate_;
      private World world_;
      private ListViewItem totalItem_;
      private List<SallyConvoy> convoys_ = new List<SallyConvoy>();
      private List<SallyCity> cities_ = new List<SallyCity>();
      private Dictionary<Material, ListViewItem> items_ = new Dictionary<Material, ListViewItem>();
      private bool frozenHarbours_;
      private int recursionLockSelectedIndexChanged_ = 0;
      private Font normalFont_;
      private Font italicFont_;
   }
}
